{
 "cells": [
  {
   "cell_type": "code",
   "execution_count": 25,
   "id": "dad90411-0670-4bec-b867-919220130567",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2024-08-24T09:02:36.486828Z",
     "iopub.status.busy": "2024-08-24T09:02:36.486198Z",
     "iopub.status.idle": "2024-08-24T09:02:36.494682Z",
     "shell.execute_reply": "2024-08-24T09:02:36.493014Z",
     "shell.execute_reply.started": "2024-08-24T09:02:36.486776Z"
    }
   },
   "outputs": [],
   "source": [
    "from IPython.display import Image"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "049bea61-a40c-4f93-b4a4-5b7c2486d8de",
   "metadata": {},
   "source": [
    "LangChain => LangGraph\n",
    "\n",
    "- LangChain 的链（Chain）不具备“循环”能力；\n",
    "- AgentExecutor 调度的Agent运行过于“黑盒”。\n",
    "    - llm with tool executor\n",
    "\n",
    "LangGraph vs. AutoGen\n",
    "\n",
    "- 都是 Multi-agent framework\n",
    "- LangGraph prefers an approach where you explicitly define different agents and transition probabilities, preferring to represent it as a graph\n",
    "    - 关于如何在 graph 中实现 transition probability，及从一个 node 跳转到另一个 node 未必一定是确定性的\n",
    "        - 其实就是 conditional edge（`workflow.add_conditional_edges`）\n",
    "            - workflow.add_edge：确定性转移边；\n",
    "            - workflow.add_conditional_edges：非确定性/概率性转移边；\n",
    "        - 条件边的实现一种经典的做法也是通过 llm 基于上下文做决策的\n",
    "        - 比如本 tutorial 里的，`是否有幻觉`的检测，`是否真正回答了用户的 query` 的检测；\n",
    "- Autogen frames it more as a \"conversation\". \n",
    "- Another key difference between Autogen and LangGraph is that LangGraph is fully integrated into the LangChain ecosystem, meaning you take fully advantage of all the LangChain integrations and LangSmith observability.\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "80d54b5f-8e14-40be-bf8e-cdd5e536b1c4",
   "metadata": {},
   "source": [
    "## (Self-Corrective) RAG on LangGraph"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "d9f1347b-9dfd-48b0-9668-cf0054ed1f65",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2024-08-24T08:55:28.421074Z",
     "iopub.status.busy": "2024-08-24T08:55:28.420443Z",
     "iopub.status.idle": "2024-08-24T08:55:28.431232Z",
     "shell.execute_reply": "2024-08-24T08:55:28.429131Z",
     "shell.execute_reply.started": "2024-08-24T08:55:28.421020Z"
    }
   },
   "source": [
    "https://github.com/vbarda/pandas-rag-langgraph/blob/main/demo.ipynb\n",
    "\n",
    "- RAG（Retrieval-Augmented Generation）\n",
    "    - 未被 llm 训练过程覆盖的 domain knowledge 或者新知识；\n",
    "    - 提供确定性的知识作为 context，进一步降低幻觉（hallucinations）\n",
    "        - GROUNDED IN DOCUMENTS\n",
    "- vector database\n",
    "    - Chroma"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "db3009c3-c424-4c60-a8f7-8857755f6a4b",
   "metadata": {},
   "source": [
    "#### RAG chain: developer-defined control flow"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 26,
   "id": "603f0060-d297-45e3-801b-7d975b356e43",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2024-08-24T09:02:41.457734Z",
     "iopub.status.busy": "2024-08-24T09:02:41.457129Z",
     "iopub.status.idle": "2024-08-24T09:02:41.469241Z",
     "shell.execute_reply": "2024-08-24T09:02:41.467584Z",
     "shell.execute_reply.started": "2024-08-24T09:02:41.457645Z"
    }
   },
   "outputs": [
    {
     "data": {
      "text/html": [
       "<img src=\"../../../imgs/rag-chain.png\" width=\"500\"/>"
      ],
      "text/plain": [
       "<IPython.core.display.Image object>"
      ]
     },
     "execution_count": 26,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "Image(url='../../../imgs/rag-chain.png', width=500)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "0836e103-bd3f-4e78-98c7-e0223001dd02",
   "metadata": {},
   "source": [
    "#### RAG Agent: LLM-defined control flow"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 27,
   "id": "20e8b105-27d9-41e7-9fa8-9585c1238d1d",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2024-08-24T09:03:06.937305Z",
     "iopub.status.busy": "2024-08-24T09:03:06.936668Z",
     "iopub.status.idle": "2024-08-24T09:03:06.949334Z",
     "shell.execute_reply": "2024-08-24T09:03:06.947731Z",
     "shell.execute_reply.started": "2024-08-24T09:03:06.937248Z"
    }
   },
   "outputs": [
    {
     "data": {
      "text/html": [
       "<img src=\"../../../imgs/rag-agent.png\" width=\"500\"/>"
      ],
      "text/plain": [
       "<IPython.core.display.Image object>"
      ]
     },
     "execution_count": 27,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "Image(url='../../../imgs/rag-agent.png', width=500)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "219a8a29-76fe-4ba2-a941-26fa1fe7d5bf",
   "metadata": {},
   "source": [
    "#### Self-Corrective RAG "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 28,
   "id": "1de5615f-8cc8-4c4c-8890-cba9b31ed32d",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2024-08-24T09:03:42.981874Z",
     "iopub.status.busy": "2024-08-24T09:03:42.981225Z",
     "iopub.status.idle": "2024-08-24T09:03:42.992532Z",
     "shell.execute_reply": "2024-08-24T09:03:42.990730Z",
     "shell.execute_reply.started": "2024-08-24T09:03:42.981820Z"
    }
   },
   "outputs": [
    {
     "data": {
      "text/html": [
       "<img src=\"../../../imgs/self-corrective-rag.png\" width=\"500\"/>"
      ],
      "text/plain": [
       "<IPython.core.display.Image object>"
      ]
     },
     "execution_count": 28,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "Image(url='../../../imgs/self-corrective-rag.png', width=500)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "id": "3b0b70e0-4bb8-48bd-aae6-112a107ad4b2",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2024-08-24T06:08:41.956293Z",
     "iopub.status.busy": "2024-08-24T06:08:41.955526Z",
     "iopub.status.idle": "2024-08-24T06:08:41.963769Z",
     "shell.execute_reply": "2024-08-24T06:08:41.962205Z",
     "shell.execute_reply.started": "2024-08-24T06:08:41.956239Z"
    }
   },
   "outputs": [],
   "source": [
    "# !pip install langgraph-checkpoint-sqlite\n",
    "# !pip install beautifulsoup4\n",
    "# !pip install chromadb"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "id": "d1a5e63c-cb44-466b-9234-f443b4397013",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2024-08-24T06:08:41.965950Z",
     "iopub.status.busy": "2024-08-24T06:08:41.965245Z",
     "iopub.status.idle": "2024-08-24T06:08:42.774491Z",
     "shell.execute_reply": "2024-08-24T06:08:42.773841Z",
     "shell.execute_reply.started": "2024-08-24T06:08:41.965900Z"
    }
   },
   "outputs": [
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "USER_AGENT environment variable not set, consider setting it to identify your requests.\n"
     ]
    }
   ],
   "source": [
    "import re\n",
    "from typing import Annotated, Iterator, Literal, TypedDict\n",
    "\n",
    "from langchain import hub\n",
    "\n",
    "# llm\n",
    "# from langchain_anthropic import ChatAnthropic\n",
    "from langchain_openai import ChatOpenAI\n",
    "\n",
    "# tool, \n",
    "# https://python.langchain.com/v0.2/docs/integrations/tools/tavily_search/\n",
    "# TAVILY_API_KEY\n",
    "from langchain_community.tools.tavily_search import TavilySearchResults\n",
    "\n",
    "# rag\n",
    "from langchain_community.document_loaders import web_base\n",
    "from langchain_community.vectorstores import Chroma\n",
    "from langchain_core.documents import Document\n",
    "from langchain_openai import OpenAIEmbeddings\n",
    "from langchain_core.retrievers import BaseRetriever\n",
    "\n",
    "# messages & prompts\n",
    "from langchain_core.messages import BaseMessage, AIMessage, convert_to_messages\n",
    "from langchain_core.prompts import ChatPromptTemplate\n",
    "\n",
    "from langchain_core.output_parsers import StrOutputParser\n",
    "from langchain_core.pydantic_v1 import BaseModel, Field\n",
    "from langchain_text_splitters import RecursiveCharacterTextSplitter\n",
    "\n",
    "# langgraph\n",
    "from langgraph.graph import END, StateGraph, add_messages\n",
    "\n",
    "from langgraph.checkpoint.memory import MemorySaver"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "id": "d14c0521-dc53-47e6-a79f-a90aa2bc7525",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2024-08-24T06:08:42.775254Z",
     "iopub.status.busy": "2024-08-24T06:08:42.775081Z",
     "iopub.status.idle": "2024-08-24T06:08:42.784172Z",
     "shell.execute_reply": "2024-08-24T06:08:42.783586Z",
     "shell.execute_reply.started": "2024-08-24T06:08:42.775240Z"
    }
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "True"
      ]
     },
     "execution_count": 3,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "import os\n",
    "os.environ['http_proxy'] = 'http://127.0.0.1:7890'\n",
    "os.environ['https_proxy'] = 'http://127.0.0.1:7890'\n",
    "from dotenv import load_dotenv\n",
    "load_dotenv()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "1c8abdf9-c2bb-4532-b234-7ed2877ea7bb",
   "metadata": {},
   "source": [
    "## model, retriever & tools"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "id": "726aaac9-a161-4cb5-8086-3da0aeaaaa0e",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2024-08-24T06:08:42.784794Z",
     "iopub.status.busy": "2024-08-24T06:08:42.784672Z",
     "iopub.status.idle": "2024-08-24T06:08:42.795961Z",
     "shell.execute_reply": "2024-08-24T06:08:42.795374Z",
     "shell.execute_reply.started": "2024-08-24T06:08:42.784782Z"
    }
   },
   "outputs": [],
   "source": [
    "SOURCE_URLS = [\n",
    "    'https://pandas.pydata.org/docs/user_guide/indexing.html',\n",
    "    'https://pandas.pydata.org/docs/user_guide/groupby.html',\n",
    "    'https://pandas.pydata.org/docs/user_guide/merging.html'\n",
    "]\n",
    "NEWLINE_RE = re.compile(\"\\n+\")\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "id": "9915336c-bcc5-4305-a06e-3cbf89381b2c",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2024-08-24T06:08:42.796644Z",
     "iopub.status.busy": "2024-08-24T06:08:42.796517Z",
     "iopub.status.idle": "2024-08-24T06:08:42.805149Z",
     "shell.execute_reply": "2024-08-24T06:08:42.804548Z",
     "shell.execute_reply.started": "2024-08-24T06:08:42.796632Z"
    }
   },
   "outputs": [],
   "source": [
    "class PandasDocsLoader(web_base.WebBaseLoader):\n",
    "    def lazy_load(self) -> Iterator[Document]:\n",
    "        \"\"\"Lazy load text from the url(s) in web_path.\"\"\"\n",
    "        for path in self.web_paths:\n",
    "            soup = self._scrape(path, bs_kwargs=self.bs_kwargs)\n",
    "            text = soup.get_text(**self.bs_get_text_kwargs)\n",
    "            text = NEWLINE_RE.sub(\"\\n\", text)     \n",
    "            metadata = web_base._build_metadata(soup, path)\n",
    "            yield Document(page_content=text, metadata=metadata)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "id": "4aed5579-cf80-4644-a173-588105fcd1c7",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2024-08-24T06:08:42.806301Z",
     "iopub.status.busy": "2024-08-24T06:08:42.806166Z",
     "iopub.status.idle": "2024-08-24T06:08:42.814861Z",
     "shell.execute_reply": "2024-08-24T06:08:42.814271Z",
     "shell.execute_reply.started": "2024-08-24T06:08:42.806289Z"
    }
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "['https://pandas.pydata.org/docs/user_guide/indexing.html',\n",
       " 'https://pandas.pydata.org/docs/user_guide/groupby.html',\n",
       " 'https://pandas.pydata.org/docs/user_guide/merging.html']"
      ]
     },
     "execution_count": 6,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "PandasDocsLoader(SOURCE_URLS).web_paths"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "id": "949ca922-08b0-417e-8cbe-aa4135d0d938",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2024-08-24T06:08:42.815518Z",
     "iopub.status.busy": "2024-08-24T06:08:42.815385Z",
     "iopub.status.idle": "2024-08-24T06:08:42.822283Z",
     "shell.execute_reply": "2024-08-24T06:08:42.821644Z",
     "shell.execute_reply.started": "2024-08-24T06:08:42.815504Z"
    }
   },
   "outputs": [],
   "source": [
    "def prepare_documents(urls: list[str]) -> list[Document]:\n",
    "    text_splitter = RecursiveCharacterTextSplitter(\n",
    "        separators=[\n",
    "            r\"In \\[[0-9]+\\]\",\n",
    "            r\"\\n+\",\n",
    "            r\"\\s+\"\n",
    "        ],\n",
    "        is_separator_regex=True,\n",
    "        chunk_size=1000\n",
    "    )\n",
    "    docs = [PandasDocsLoader(url).load() for url in urls]\n",
    "    docs_list = [item for sublist in docs for item in sublist]\n",
    "    return text_splitter.split_documents(docs_list)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "id": "6ea55ac6-2c79-49d6-8605-20fc634af230",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2024-08-24T06:08:42.823040Z",
     "iopub.status.busy": "2024-08-24T06:08:42.822878Z",
     "iopub.status.idle": "2024-08-24T06:08:42.833839Z",
     "shell.execute_reply": "2024-08-24T06:08:42.833196Z",
     "shell.execute_reply.started": "2024-08-24T06:08:42.823024Z"
    }
   },
   "outputs": [],
   "source": [
    "def get_retriever() -> BaseRetriever:\n",
    "    documents = prepare_documents(SOURCE_URLS)\n",
    "    vectorstore = Chroma.from_documents(\n",
    "        documents=documents,\n",
    "        collection_name=\"pandas-rag-chroma\",\n",
    "        embedding=OpenAIEmbeddings(),\n",
    "    )\n",
    "    retriever = vectorstore.as_retriever()\n",
    "    return retriever"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "id": "f088d2fc-bb6f-46ba-b73d-ef856ca9a093",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2024-08-24T06:08:42.834702Z",
     "iopub.status.busy": "2024-08-24T06:08:42.834512Z",
     "iopub.status.idle": "2024-08-24T06:08:53.466345Z",
     "shell.execute_reply": "2024-08-24T06:08:53.465455Z",
     "shell.execute_reply.started": "2024-08-24T06:08:42.834683Z"
    }
   },
   "outputs": [],
   "source": [
    "llm = ChatOpenAI(model=\"gpt-4o\", temperature=0)\n",
    "retriever = get_retriever()\n",
    "tavily_search_tool = TavilySearchResults(max_results=3)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "42ad68a0-0379-470f-bc35-493a1122ffd2",
   "metadata": {},
   "source": [
    "## Graph"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 43,
   "id": "289a5dca-1716-42c5-8df5-c16ac97b0963",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2024-08-24T10:34:52.252084Z",
     "iopub.status.busy": "2024-08-24T10:34:52.251452Z",
     "iopub.status.idle": "2024-08-24T10:34:52.263327Z",
     "shell.execute_reply": "2024-08-24T10:34:52.261692Z",
     "shell.execute_reply.started": "2024-08-24T10:34:52.252031Z"
    }
   },
   "outputs": [
    {
     "data": {
      "text/html": [
       "<img src=\"../../../imgs/self-corrective-rag.png\" width=\"500\"/>"
      ],
      "text/plain": [
       "<IPython.core.display.Image object>"
      ]
     },
     "execution_count": 43,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "Image(url='../../../imgs/self-corrective-rag.png', width=500)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "1c863c65-535a-480a-b5a1-88eebe9565b3",
   "metadata": {},
   "source": [
    "- state: graph 中所有 node 的输入\n",
    "    - question: user query\n",
    "    - messages: add\n",
    "    - documents: 基于 `retriever.invoke(question)` or `search_tool`\n",
    "    - candidate_answer: generate\n",
    "    - retries\n",
    "    - web_fallback\n",
    "- nodes: 接收状态，执行动作，产生/改变状态\n",
    "    - rewrite question: 单独的一个重写用户 query 的 llm 调用\n",
    "    - document_search: retriever\n",
    "        - append documents\n",
    "    - generate: llm chain (lcel)\n",
    "        - 提供或者替换 candidate_answer\n",
    "    - web search: search tool\n",
    "        - append documents\n",
    "    - finalize response"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "224a3b17-08a7-407e-a1d3-1d43811e1354",
   "metadata": {},
   "source": [
    "### state"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "id": "4a91005b-47cf-4c73-bdbe-8d8aa8a39fbb",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2024-08-24T08:36:55.295511Z",
     "iopub.status.busy": "2024-08-24T08:36:55.295199Z",
     "iopub.status.idle": "2024-08-24T08:36:55.303247Z",
     "shell.execute_reply": "2024-08-24T08:36:55.301714Z",
     "shell.execute_reply.started": "2024-08-24T08:36:55.295487Z"
    }
   },
   "outputs": [],
   "source": [
    "class GraphState(TypedDict):\n",
    "    messages: Annotated[list[BaseMessage], add_messages]\n",
    "    question: str\n",
    "    documents: list[Document]\n",
    "    candidate_answer: str\n",
    "    retries: int\n",
    "    web_fallback: bool\n",
    "\n",
    "\n",
    "class GraphConfig(TypedDict):\n",
    "    max_retries: int"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 15,
   "id": "0ec3791b-15d6-46c9-9016-94cafb848334",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2024-08-24T08:38:19.709714Z",
     "iopub.status.busy": "2024-08-24T08:38:19.709038Z",
     "iopub.status.idle": "2024-08-24T08:38:19.717986Z",
     "shell.execute_reply": "2024-08-24T08:38:19.716298Z",
     "shell.execute_reply.started": "2024-08-24T08:38:19.709611Z"
    }
   },
   "outputs": [],
   "source": [
    "MAX_RETRIES = 3\n",
    "VERBOSE = True"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "fac6f777-02a6-4d60-bc29-2243c6db90f1",
   "metadata": {},
   "source": [
    "### nodes"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "733f7bc6-5ce6-44a4-8214-d0a19da5a9db",
   "metadata": {},
   "source": [
    "#### document search node"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 16,
   "id": "81efd9a8-0f19-4c22-be93-8ac7411e1bd2",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2024-08-24T08:38:27.599973Z",
     "iopub.status.busy": "2024-08-24T08:38:27.599340Z",
     "iopub.status.idle": "2024-08-24T08:38:27.609734Z",
     "shell.execute_reply": "2024-08-24T08:38:27.607877Z",
     "shell.execute_reply.started": "2024-08-24T08:38:27.599920Z"
    }
   },
   "outputs": [],
   "source": [
    "def document_search(state: GraphState):\n",
    "    \"\"\"\n",
    "    Retrieve documents\n",
    "\n",
    "    Args:\n",
    "        state (dict): The current graph state\n",
    "\n",
    "    Returns:\n",
    "        state (dict): New key added to state, documents, that contains retrieved documents\n",
    "    \"\"\"\n",
    "    if VERBOSE:\n",
    "        print(\"---RETRIEVE---\")\n",
    "\n",
    "    question = convert_to_messages(state[\"messages\"])[-1].content\n",
    "\n",
    "    # Retrieval\n",
    "    documents = retriever.invoke(question)\n",
    "    return {\"documents\": documents, \"question\": question, \"web_fallback\": True}"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 18,
   "id": "2d555ffa-5916-4b50-89e2-055c3f2a75bf",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2024-08-24T08:40:01.783933Z",
     "iopub.status.busy": "2024-08-24T08:40:01.781842Z",
     "iopub.status.idle": "2024-08-24T08:40:02.854696Z",
     "shell.execute_reply": "2024-08-24T08:40:02.852557Z",
     "shell.execute_reply.started": "2024-08-24T08:40:01.783842Z"
    }
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "ChatPromptTemplate(input_variables=['context', 'question'], metadata={'lc_hub_owner': 'rlm', 'lc_hub_repo': 'rag-prompt', 'lc_hub_commit_hash': '50442af133e61576e74536c6556cefe1fac147cad032f4377b60c436e6cdcb6e'}, messages=[HumanMessagePromptTemplate(prompt=PromptTemplate(input_variables=['context', 'question'], template=\"You are an assistant for question-answering tasks. Use the following pieces of retrieved context to answer the question. If you don't know the answer, just say that you don't know. Use three sentences maximum and keep the answer concise.\\nQuestion: {question} \\nContext: {context} \\nAnswer:\"))])"
      ]
     },
     "execution_count": 18,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "# https://smith.langchain.com/hub/rlm/rag-prompt\n",
    "# RAG: QA with context, answer the question base the context\n",
    "RAG_PROMPT: ChatPromptTemplate = hub.pull(\"rlm/rag-prompt\")\n",
    "RAG_PROMPT"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c9dfc1d3-e030-4682-b196-811c103ff4ab",
   "metadata": {},
   "source": [
    "#### generate node"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 19,
   "id": "d54f850b-da8e-4d28-ace2-60027927aac0",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2024-08-24T08:41:19.260315Z",
     "iopub.status.busy": "2024-08-24T08:41:19.259677Z",
     "iopub.status.idle": "2024-08-24T08:41:19.271431Z",
     "shell.execute_reply": "2024-08-24T08:41:19.269566Z",
     "shell.execute_reply.started": "2024-08-24T08:41:19.260261Z"
    }
   },
   "outputs": [],
   "source": [
    "def generate(state: GraphState):\n",
    "    \"\"\"\n",
    "    Generate answer\n",
    "\n",
    "    Args:\n",
    "        state (dict): The current graph state\n",
    "\n",
    "    Returns:\n",
    "        state (dict): New key added to state, generation, that contains LLM generation\n",
    "    \"\"\"\n",
    "    if VERBOSE:\n",
    "        print(\"---GENERATE---\")\n",
    "    question = state[\"question\"]\n",
    "    documents = state[\"documents\"]\n",
    "    retries = state[\"retries\"] if state.get(\"retries\") is not None else -1\n",
    "\n",
    "    # lcel\n",
    "    rag_chain = RAG_PROMPT | llm | StrOutputParser()\n",
    "    generation = rag_chain.invoke({\"context\": documents, \"question\": question})\n",
    "    return {\"retries\": retries + 1, \"candidate_answer\": generation}"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "8f74fd7e-cd7d-4087-a871-dfe235999f91",
   "metadata": {},
   "source": [
    "#### rewrite question"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 20,
   "id": "38c6800d-5b53-4213-b2d9-04202d565bad",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2024-08-24T08:45:57.713065Z",
     "iopub.status.busy": "2024-08-24T08:45:57.712428Z",
     "iopub.status.idle": "2024-08-24T08:45:57.723662Z",
     "shell.execute_reply": "2024-08-24T08:45:57.721415Z",
     "shell.execute_reply.started": "2024-08-24T08:45:57.713011Z"
    }
   },
   "outputs": [],
   "source": [
    "QUERY_REWRITER_SYSTEM = (\n",
    "\"\"\"\n",
    "You are a question re-writer that converts an input question to a better version that is optimized for vectorstore retrieval.\n",
    "Look at the input and try to reason about the underlying semantic intent / meaning.\n",
    "\"\"\"\n",
    ")\n",
    "\n",
    "QUERY_REWRITER_PROMPT = ChatPromptTemplate.from_messages(\n",
    "    [\n",
    "        (\"system\", QUERY_REWRITER_SYSTEM),\n",
    "        (\n",
    "            \"human\",\n",
    "            \"Here is the initial question: \\n\\n {question} \\n Formulate an improved question.\",\n",
    "        ),\n",
    "    ]\n",
    ")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 21,
   "id": "28a7d287-5383-48c7-a50d-659d43eadfd7",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2024-08-24T08:46:37.796177Z",
     "iopub.status.busy": "2024-08-24T08:46:37.794601Z",
     "iopub.status.idle": "2024-08-24T08:46:37.805150Z",
     "shell.execute_reply": "2024-08-24T08:46:37.803230Z",
     "shell.execute_reply.started": "2024-08-24T08:46:37.796111Z"
    }
   },
   "outputs": [],
   "source": [
    "def transform_query(state: GraphState):\n",
    "    \"\"\"\n",
    "    Transform the query to produce a better question.\n",
    "\n",
    "    Args:\n",
    "        state (dict): The current graph state\n",
    "\n",
    "    Returns:\n",
    "        state (dict): Updates question key with a re-phrased question\n",
    "    \"\"\"\n",
    "    if VERBOSE:\n",
    "        print(\"---TRANSFORM QUERY---\")\n",
    "\n",
    "    question = state[\"question\"]\n",
    "\n",
    "    # Re-write question\n",
    "    query_rewriter = QUERY_REWRITER_PROMPT | llm | StrOutputParser()\n",
    "    better_question = query_rewriter.invoke({\"question\": question})\n",
    "    return {\"question\": better_question}"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "343dba90-a144-4b35-aa03-27303fba8938",
   "metadata": {},
   "source": [
    "#### web search"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 22,
   "id": "dcf6359b-7833-4787-8f2a-cdf359ca57a8",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2024-08-24T08:49:57.951225Z",
     "iopub.status.busy": "2024-08-24T08:49:57.950603Z",
     "iopub.status.idle": "2024-08-24T08:49:57.962707Z",
     "shell.execute_reply": "2024-08-24T08:49:57.960766Z",
     "shell.execute_reply.started": "2024-08-24T08:49:57.951173Z"
    }
   },
   "outputs": [],
   "source": [
    "def web_search(state: GraphState):\n",
    "    if VERBOSE:\n",
    "        print(\"---RUNNING WEB SEARCH---\")\n",
    "\n",
    "    question = state[\"question\"]\n",
    "    documents = state[\"documents\"]\n",
    "    search_results = tavily_search_tool.invoke(question)\n",
    "    search_content = \"\\n\".join([d[\"content\"] for d in search_results])\n",
    "    documents.append(Document(page_content=search_content, metadata={\"source\": \"websearch\"}))\n",
    "    return {\"documents\": documents, \"web_fallback\": False}"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "f801d17c-7de2-412a-bfee-58558e87575a",
   "metadata": {},
   "source": [
    "#### finalize resp"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 23,
   "id": "37892afd-2e95-445b-b440-f5e86d52c66d",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2024-08-24T08:53:25.107490Z",
     "iopub.status.busy": "2024-08-24T08:53:25.106861Z",
     "iopub.status.idle": "2024-08-24T08:53:25.115709Z",
     "shell.execute_reply": "2024-08-24T08:53:25.114313Z",
     "shell.execute_reply.started": "2024-08-24T08:53:25.107438Z"
    }
   },
   "outputs": [],
   "source": [
    "def finalize_response(state: GraphState):\n",
    "    if VERBOSE:\n",
    "        print(\"---FINALIZING THE RESPONSE---\")\n",
    "\n",
    "    return {\"messages\": [AIMessage(content=state[\"candidate_answer\"])]}"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "54dfa224-0edb-4a75-8b5b-6830ed475688",
   "metadata": {},
   "source": [
    "### edges & graph"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 42,
   "id": "df6a64b1-7613-4513-82ad-dcf6980f2a2f",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2024-08-24T10:26:54.627026Z",
     "iopub.status.busy": "2024-08-24T10:26:54.626384Z",
     "iopub.status.idle": "2024-08-24T10:26:54.638907Z",
     "shell.execute_reply": "2024-08-24T10:26:54.637019Z",
     "shell.execute_reply.started": "2024-08-24T10:26:54.626971Z"
    }
   },
   "outputs": [
    {
     "data": {
      "text/html": [
       "<img src=\"../../../imgs/self-corrective-rag.png\" width=\"500\"/>"
      ],
      "text/plain": [
       "<IPython.core.display.Image object>"
      ]
     },
     "execution_count": 42,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "Image(url='../../../imgs/self-corrective-rag.png', width=500)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "7002eab1-ad1b-4b87-9c6f-1e256cada10f",
   "metadata": {},
   "source": [
    "#### Grade answer"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "33f59c66-4e12-4273-a954-810094d508fb",
   "metadata": {},
   "source": [
    "- Check hallucinations\n",
    "- Check answer relevance"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 29,
   "id": "7618f755-4902-487e-9098-888a3b6221b2",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2024-08-24T09:10:31.720734Z",
     "iopub.status.busy": "2024-08-24T09:10:31.720103Z",
     "iopub.status.idle": "2024-08-24T09:10:31.736622Z",
     "shell.execute_reply": "2024-08-24T09:10:31.734774Z",
     "shell.execute_reply.started": "2024-08-24T09:10:31.720680Z"
    }
   },
   "outputs": [],
   "source": [
    "class GradeHallucinations(BaseModel):\n",
    "    \"\"\"Binary score for hallucination present in generation answer.\"\"\"\n",
    "\n",
    "    binary_score: str = Field(\n",
    "        description=\"Answer is grounded in the facts, 'yes' or 'no'\"\n",
    "    )\n",
    "\n",
    "\n",
    "HALLUCINATION_GRADER_SYSTEM = (\n",
    "\"\"\"\n",
    "You are a grader assessing whether an LLM generation is grounded in / supported by a set of retrieved facts.\n",
    "Give a binary score 'yes' or 'no', where 'yes' means that the answer is grounded in / supported by the set of facts.\n",
    "\n",
    "IF the generation includes code examples, make sure those examples are FULLY present in the set of facts, otherwise always return score 'no'.\n",
    "\"\"\"\n",
    ")\n",
    "\n",
    "HALLUCINATION_GRADER_PROMPT = ChatPromptTemplate.from_messages(\n",
    "    [\n",
    "        (\"system\", HALLUCINATION_GRADER_SYSTEM),\n",
    "        (\"human\", \"Set of facts: \\n\\n {documents} \\n\\n LLM generation: {generation}\"),\n",
    "    ]\n",
    ")\n",
    "\n",
    "\n",
    "class GradeAnswer(BaseModel):\n",
    "    \"\"\"Binary score to assess answer addresses question.\"\"\"\n",
    "\n",
    "    binary_score: str = Field(\n",
    "        description=\"Answer addresses the question, 'yes' or 'no'\"\n",
    "    )\n",
    "\n",
    "\n",
    "ANSWER_GRADER_SYSTEM = (\n",
    "\"\"\"\n",
    "You are a grader assessing whether an answer addresses / resolves a question.\n",
    "Give a binary score 'yes' or 'no', where 'yes' means that the answer resolves the question.\n",
    "\"\"\"\n",
    ")\n",
    "\n",
    "ANSWER_GRADER_PROMPT = ChatPromptTemplate.from_messages(\n",
    "    [\n",
    "        (\"system\", ANSWER_GRADER_SYSTEM),\n",
    "        (\"human\", \"User question: \\n\\n {question} \\n\\n LLM generation: {generation}\"),\n",
    "    ]\n",
    ")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 30,
   "id": "94a25614-efb0-43ef-9771-8c7abce3d193",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2024-08-24T09:11:17.109534Z",
     "iopub.status.busy": "2024-08-24T09:11:17.108899Z",
     "iopub.status.idle": "2024-08-24T09:11:17.126366Z",
     "shell.execute_reply": "2024-08-24T09:11:17.124317Z",
     "shell.execute_reply.started": "2024-08-24T09:11:17.109480Z"
    }
   },
   "outputs": [],
   "source": [
    "def grade_generation_v_documents_and_question(state: GraphState, config) -> Literal[\"generate\", \"transform_query\", \"web_search\", \"finalize_response\"]:\n",
    "    \"\"\"\n",
    "    Determines whether the generation is grounded in the document and answers question.\n",
    "\n",
    "    Args:\n",
    "        state (dict): The current graph state\n",
    "\n",
    "    Returns:\n",
    "        str: Decision for next node to call\n",
    "    \"\"\"\n",
    "    question = state[\"question\"]\n",
    "    documents = state[\"documents\"]\n",
    "    generation = state[\"candidate_answer\"]\n",
    "    web_fallback = state[\"web_fallback\"]\n",
    "    retries = state[\"retries\"] if state.get(\"retries\") is not None else -1\n",
    "    max_retries = config.get(\"configurable\", {}).get(\"max_retries\", MAX_RETRIES)\n",
    "\n",
    "    # this means we've already gone through web fallback and can return to the user\n",
    "    if not web_fallback:\n",
    "        return \"finalize_response\"\n",
    "\n",
    "    if VERBOSE:\n",
    "        print(\"---CHECK HALLUCINATIONS---\")\n",
    "\n",
    "    # llm lcel chain\n",
    "    hallucination_grader = HALLUCINATION_GRADER_PROMPT | llm.with_structured_output(GradeHallucinations)\n",
    "    hallucination_grade: GradeHallucinations = hallucination_grader.invoke(\n",
    "        {\"documents\": documents, \"generation\": generation}\n",
    "    )\n",
    "\n",
    "    # Check hallucination\n",
    "    if hallucination_grade.binary_score == \"no\":\n",
    "        if VERBOSE: print(\"---DECISION: GENERATION IS NOT GROUNDED IN DOCUMENTS, RE-TRY---\")\n",
    "        return \"generate\" if retries < max_retries else \"web_search\"\n",
    "\n",
    "    if VERBOSE:\n",
    "        print(\"---DECISION: GENERATION IS GROUNDED IN DOCUMENTS---\")\n",
    "        print(\"---GRADE GENERATION vs QUESTION---\")\n",
    "\n",
    "    # Check question-answering\n",
    "    answer_grader = ANSWER_GRADER_PROMPT | llm.with_structured_output(GradeAnswer)\n",
    "    answer_grade: GradeAnswer = answer_grader.invoke({\"question\": question, \"generation\": generation})\n",
    "    if answer_grade.binary_score == \"yes\":\n",
    "        if VERBOSE: print(\"---DECISION: GENERATION ADDRESSES QUESTION---\")\n",
    "        return \"finalize_response\"\n",
    "    else:\n",
    "        if VERBOSE: print(\"---DECISION: GENERATION DOES NOT ADDRESS QUESTION---\")\n",
    "        return \"transform_query\" if retries < max_retries else \"web_search\""
   ]
  },
  {
   "cell_type": "markdown",
   "id": "ac8968d7-4e35-4df9-bc1a-2aa5b3874063",
   "metadata": {},
   "source": [
    "条件性跳转（from `generate`），基于的幻觉检测（llm lcel chain invoke）\n",
    "- \"generate\",\n",
    "- \"transform_query\",\n",
    "- \"web_search\",\n",
    "- \"finalize_response\""
   ]
  },
  {
   "cell_type": "markdown",
   "id": "ea7e7b3c-d6de-41e2-8fad-df442c597628",
   "metadata": {},
   "source": [
    "#### build the graph"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 31,
   "id": "a486639f-9cf4-4b39-a9c3-7599461da73e",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2024-08-24T09:12:23.597041Z",
     "iopub.status.busy": "2024-08-24T09:12:23.596398Z",
     "iopub.status.idle": "2024-08-24T09:12:23.616668Z",
     "shell.execute_reply": "2024-08-24T09:12:23.614534Z",
     "shell.execute_reply.started": "2024-08-24T09:12:23.596986Z"
    }
   },
   "outputs": [],
   "source": [
    "workflow = StateGraph(GraphState, config_schema=GraphConfig)\n",
    "\n",
    "# Define the nodes\n",
    "workflow.add_node(\"document_search\", document_search)\n",
    "workflow.add_node(\"generate\", generate)\n",
    "workflow.add_node(\"transform_query\", transform_query)\n",
    "workflow.add_node(\"web_search\", web_search)\n",
    "workflow.add_node(\"finalize_response\", finalize_response)\n",
    "\n",
    "# Build graph\n",
    "workflow.set_entry_point(\"document_search\")\n",
    "workflow.add_edge(\"document_search\", \"generate\")\n",
    "workflow.add_edge(\"transform_query\", \"document_search\")\n",
    "workflow.add_edge(\"web_search\", \"generate\")\n",
    "workflow.add_edge(\"finalize_response\", END)\n",
    "\n",
    "workflow.add_conditional_edges(\n",
    "    \"generate\",\n",
    "    grade_generation_v_documents_and_question\n",
    ")\n",
    "\n",
    "# Compile\n",
    "graph = workflow.compile()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 35,
   "id": "ce35861d-30c8-4590-af40-35f2aa8c8a16",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2024-08-24T09:12:55.582543Z",
     "iopub.status.busy": "2024-08-24T09:12:55.581916Z",
     "iopub.status.idle": "2024-08-24T09:12:55.590705Z",
     "shell.execute_reply": "2024-08-24T09:12:55.588815Z",
     "shell.execute_reply.started": "2024-08-24T09:12:55.582490Z"
    }
   },
   "outputs": [],
   "source": [
    "from IPython.display import Image, display"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 36,
   "id": "8b881faf-afe8-4b67-959e-162d4afdedf4",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2024-08-24T09:12:57.187887Z",
     "iopub.status.busy": "2024-08-24T09:12:57.187262Z",
     "iopub.status.idle": "2024-08-24T09:12:57.550331Z",
     "shell.execute_reply": "2024-08-24T09:12:57.548454Z",
     "shell.execute_reply.started": "2024-08-24T09:12:57.187835Z"
    }
   },
   "outputs": [
    {
     "data": {
      "image/jpeg": "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",
      "text/plain": [
       "<IPython.core.display.Image object>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "# grade_generation_v_documents_and_question\n",
    "display(Image(graph.get_graph().draw_mermaid_png()))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "05ea77c4-1ee5-4411-817e-9f00fba74039",
   "metadata": {},
   "source": [
    "### run the graph"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 38,
   "id": "b08202c4-0b4b-402c-abd6-05648c116dca",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2024-08-24T09:46:18.863859Z",
     "iopub.status.busy": "2024-08-24T09:46:18.863208Z",
     "iopub.status.idle": "2024-08-24T09:46:23.169268Z",
     "shell.execute_reply": "2024-08-24T09:46:23.167373Z",
     "shell.execute_reply.started": "2024-08-24T09:46:18.863805Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "---RETRIEVE---\n",
      "{'document_search': {'question': 'how do i calculate sum by groups', 'documents': [Document(metadata={'language': 'en', 'source': 'https://pandas.pydata.org/docs/user_guide/groupby.html', 'title': 'Group by: split-apply-combine — pandas 2.2.2 documentation'}, page_content='Windowing operations\\nTime series / date functionality\\nTime deltas\\nOptions and settings\\nEnhancing performance\\nScaling to large datasets\\nSparse data structures\\nFrequently Asked Questions (FAQ)\\nCookbook\\nUser Guide\\nGroup by:...\\nGroup by: split-apply-combine#\\nBy “group by” we are referring to a process involving one or more of the following\\nsteps:\\nSplitting the data into groups based on some criteria.\\nApplying a function to each group independently.\\nCombining the results into a data structure.\\nOut of these, the split step is the most straightforward. In the apply step, we\\nmight wish to do one of the following:\\nAggregation: compute a summary statistic (or statistics) for each\\ngroup. Some examples:\\nCompute group sums or means.\\nCompute group sizes / counts.\\nTransformation: perform some group-specific computations and return a\\nlike-indexed object. Some examples:\\nStandardize data (zscore) within a group.\\nFilling NAs within groups with a value derived from each group.'), Document(metadata={'language': 'en', 'source': 'https://pandas.pydata.org/docs/user_guide/groupby.html', 'title': 'Group by: split-apply-combine — pandas 2.2.2 documentation'}, page_content='In [108]: grouped[\"C\"].agg([\"sum\", \"sum\"])\\nOut[108]: \\n          sum       sum\\nA                      \\nbar  0.392940  0.392940\\nfoo -1.796421 -1.796421\\npandas also allows you to provide multiple lambdas. In this case, pandas\\nwill mangle the name of the (nameless) lambda functions, appending _<i>\\nto each subsequent lambda.'), Document(metadata={'language': 'en', 'source': 'https://pandas.pydata.org/docs/user_guide/groupby.html', 'title': 'Group by: split-apply-combine — pandas 2.2.2 documentation'}, page_content='In [116]: grouped.agg({\"C\": \"sum\", \"D\": \"std\"})\\nOut[116]: \\n            C         D\\nA                      \\nbar  0.392940  1.366330\\nfoo -1.796421  0.884785\\nTransformation#\\nA transformation is a GroupBy operation whose result is indexed the same\\nas the one being grouped. Common examples include cumsum() and\\ndiff().\\nIn [117]: speeds\\nOut[117]: \\n          class           order  max_speed\\nfalcon     bird   Falconiformes      389.0\\nparrot     bird  Psittaciformes       24.0\\nlion     mammal       Carnivora       80.2\\nmonkey   mammal        Primates        NaN\\nleopard  mammal       Carnivora       58.0\\nIn [118]: grouped = speeds.groupby(\"class\")[\"max_speed\"]\\nIn [119]: grouped.cumsum()\\nOut[119]: \\nfalcon     389.0\\nparrot     413.0\\nlion        80.2\\nmonkey       NaN\\nleopard    138.2\\nName: max_speed, dtype: float64'), Document(metadata={'language': 'en', 'source': 'https://pandas.pydata.org/docs/user_guide/groupby.html', 'title': 'Group by: split-apply-combine — pandas 2.2.2 documentation'}, page_content='In [73]: grouped[[\"A\", \"B\"]].sum()\\nOut[73]: \\n                   A                  B\\nA                                      \\nbar        barbarbar        onethreetwo\\nfoo  foofoofoofoofoo  onetwotwoonethree\\nIterating through groups#\\nWith the GroupBy object in hand, iterating through the grouped data is very\\nnatural and functions similarly to itertools.groupby():\\nIn [74]: grouped = df.groupby(\\'A\\')\\nIn [75]: for name, group in grouped:\\n   ....:     print(name)\\n   ....:     print(group)\\n   ....: \\nbar\\n     A      B         C         D\\n1  bar    one  0.254161  1.511763\\n3  bar  three  0.215897 -0.990582\\n5  bar    two -0.077118  1.211526\\nfoo\\n     A      B         C         D\\n0  foo    one -0.575247  1.346061\\n2  foo    two -1.143704  1.627081\\n4  foo    two  1.193555 -0.441652\\n6  foo    one -0.408530  0.268520\\n7  foo  three -0.862495  0.024580\\nIn the case of grouping by multiple keys, the group name will be a tuple:')], 'web_fallback': True}}\n",
      "\n",
      "---\n",
      "\n",
      "---GENERATE---\n",
      "---CHECK HALLUCINATIONS---\n",
      "---DECISION: GENERATION IS GROUNDED IN DOCUMENTS---\n",
      "---GRADE GENERATION vs QUESTION---\n",
      "---DECISION: GENERATION ADDRESSES QUESTION---\n",
      "{'generate': {'candidate_answer': \"To calculate the sum by groups in pandas, you can use the `groupby` method followed by the `sum` method. For example, `grouped = df.groupby('column_name')` and then `grouped.sum()`. This will give you the sum of each group based on the specified column.\", 'retries': 0}}\n",
      "\n",
      "---\n",
      "\n",
      "---FINALIZING THE RESPONSE---\n",
      "{'finalize_response': {'messages': [AIMessage(content=\"To calculate the sum by groups in pandas, you can use the `groupby` method followed by the `sum` method. For example, `grouped = df.groupby('column_name')` and then `grouped.sum()`. This will give you the sum of each group based on the specified column.\")]}}\n",
      "\n",
      "---\n",
      "\n"
     ]
    }
   ],
   "source": [
    "VERBOSE = True\n",
    "inputs = {\"messages\": [(\"human\", \"how do i calculate sum by groups\")]}\n",
    "for output in graph.stream(inputs):\n",
    "    print(output)\n",
    "    print(\"\\n---\\n\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "258ff190-dae1-4e65-87f6-7b4e1220d3d1",
   "metadata": {},
   "source": [
    "#### Query with a fallback"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 40,
   "id": "8d48142f-7dbf-4654-8095-06d6f9124b6e",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2024-08-24T09:47:25.214327Z",
     "iopub.status.busy": "2024-08-24T09:47:25.213592Z",
     "iopub.status.idle": "2024-08-24T09:47:34.315603Z",
     "shell.execute_reply": "2024-08-24T09:47:34.313613Z",
     "shell.execute_reply.started": "2024-08-24T09:47:25.214252Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "---RETRIEVE---\n",
      "{'document_search': {'question': 'how do i convert a column into dummies', 'documents': [Document(metadata={'language': 'en', 'source': 'https://pandas.pydata.org/docs/user_guide/indexing.html', 'title': 'Indexing and selecting data — pandas 2.2.2 documentation'}, page_content=\"In [7]: df[['B', 'A']] = df[['A', 'B']]\\nIn [8]: df\\nOut[8]: \\n                   A         B         C         D\\n2000-01-01 -0.282863  0.469112 -1.509059 -1.135632\\n2000-01-02 -0.173215  1.212112  0.119209 -1.044236\\n2000-01-03 -2.104569 -0.861849 -0.494929  1.071804\\n2000-01-04 -0.706771  0.721555 -1.039575  0.271860\\n2000-01-05  0.567020 -0.424972  0.276232 -1.087401\\n2000-01-06  0.113648 -0.673690 -1.478427  0.524988\\n2000-01-07  0.577046  0.404705 -1.715002 -1.039268\\n2000-01-08 -1.157892 -0.370647 -1.344312  0.844885\\nYou may find this useful for applying a transform (in-place) to a subset of the\\ncolumns.\\nWarning\\npandas aligns all AXES when setting Series and DataFrame from .loc.\\nThis will not modify df because the column alignment is before value assignment.\"), Document(metadata={'language': 'en', 'source': 'https://pandas.pydata.org/docs/user_guide/indexing.html', 'title': 'Indexing and selecting data — pandas 2.2.2 documentation'}, page_content=\"In [21]: sa.a = 5\\nIn [22]: sa\\nOut[22]: \\na    5\\nb    2\\nc    3\\ndtype: int64\\nIn [23]: dfa.A = list(range(len(dfa.index)))  # ok if A already exists\\nIn [24]: dfa\\nOut[24]: \\n            A         B         C         D\\n2000-01-01  0  0.469112 -1.509059 -1.135632\\n2000-01-02  1  1.212112  0.119209 -1.044236\\n2000-01-03  2 -0.861849 -0.494929  1.071804\\n2000-01-04  3  0.721555 -1.039575  0.271860\\n2000-01-05  4 -0.424972  0.276232 -1.087401\\n2000-01-06  5 -0.673690 -1.478427  0.524988\\n2000-01-07  6  0.404705 -1.715002 -1.039268\\n2000-01-08  7 -0.370647 -1.344312  0.844885\\nIn [25]: dfa['A'] = list(range(len(dfa.index)))  # use this form to create a new column\"), Document(metadata={'language': 'en', 'source': 'https://pandas.pydata.org/docs/user_guide/merging.html', 'title': 'Merge, join, concatenate and compare — pandas 2.2.2 documentation'}, page_content='In [144]: df = pd.DataFrame(\\n   .....:     {\\n   .....:         \"col1\": [\"a\", \"a\", \"b\", \"b\", \"a\"],\\n   .....:         \"col2\": [1.0, 2.0, 3.0, np.nan, 5.0],\\n   .....:         \"col3\": [1.0, 2.0, 3.0, 4.0, 5.0],\\n   .....:     },\\n   .....:     columns=[\"col1\", \"col2\", \"col3\"],\\n   .....: )\\n   .....: \\nIn [145]: df\\nOut[145]: \\n  col1  col2  col3\\n0    a   1.0   1.0\\n1    a   2.0   2.0\\n2    b   3.0   3.0\\n3    b   NaN   4.0\\n4    a   5.0   5.0\\nIn [146]: df2 = df.copy()\\nIn [147]: df2.loc[0, \"col1\"] = \"c\"\\nIn [148]: df2.loc[2, \"col3\"] = 4.0\\nIn [149]: df2\\nOut[149]: \\n  col1  col2  col3\\n0    c   1.0   1.0\\n1    a   2.0   2.0\\n2    b   3.0   4.0\\n3    b   NaN   4.0\\n4    a   5.0   5.0'), Document(metadata={'language': 'en', 'source': 'https://pandas.pydata.org/docs/user_guide/groupby.html', 'title': 'Group by: split-apply-combine — pandas 2.2.2 documentation'}, page_content='In [225]: df\\nOut[225]: \\n  Branch Buyer  Quantity                Date\\n0      A  Carl         1 2013-01-01 13:00:00\\n1      A  Mark         3 2013-01-01 13:05:00\\n2      A  Carl         5 2013-10-01 20:00:00\\n3      A  Carl         1 2013-10-02 10:00:00\\n4      A   Joe         8 2013-10-01 20:00:00\\n5      A   Joe         1 2013-10-02 10:00:00\\n6      A   Joe         9 2013-12-02 12:00:00\\n7      B  Carl         3 2013-12-02 14:00:00\\nGroupby a specific column with the desired frequency. This is like resampling.')], 'web_fallback': True}}\n",
      "\n",
      "---\n",
      "\n",
      "---GENERATE---\n",
      "---CHECK HALLUCINATIONS---\n",
      "---DECISION: GENERATION IS NOT GROUNDED IN DOCUMENTS, RE-TRY---\n",
      "{'generate': {'candidate_answer': \"You can convert a column into dummy variables using the `pd.get_dummies()` function in pandas. For example, `pd.get_dummies(df['col1'])` will convert the 'col1' column into dummy variables. This function creates a new DataFrame with binary columns for each unique value in the original column.\", 'retries': 0}}\n",
      "\n",
      "---\n",
      "\n",
      "---GENERATE---\n",
      "---CHECK HALLUCINATIONS---\n",
      "---DECISION: GENERATION IS NOT GROUNDED IN DOCUMENTS, RE-TRY---\n",
      "{'generate': {'candidate_answer': \"You can convert a column into dummies using the `pd.get_dummies()` function in pandas. For example, if you have a DataFrame `df` and you want to convert the column `col1` into dummies, you can use `pd.get_dummies(df, columns=['col1'])`. This will create a new DataFrame with dummy variables for each unique value in `col1`.\", 'retries': 1}}\n",
      "\n",
      "---\n",
      "\n",
      "---RUNNING WEB SEARCH---\n",
      "{'web_search': {'documents': [Document(metadata={'language': 'en', 'source': 'https://pandas.pydata.org/docs/user_guide/indexing.html', 'title': 'Indexing and selecting data — pandas 2.2.2 documentation'}, page_content=\"In [7]: df[['B', 'A']] = df[['A', 'B']]\\nIn [8]: df\\nOut[8]: \\n                   A         B         C         D\\n2000-01-01 -0.282863  0.469112 -1.509059 -1.135632\\n2000-01-02 -0.173215  1.212112  0.119209 -1.044236\\n2000-01-03 -2.104569 -0.861849 -0.494929  1.071804\\n2000-01-04 -0.706771  0.721555 -1.039575  0.271860\\n2000-01-05  0.567020 -0.424972  0.276232 -1.087401\\n2000-01-06  0.113648 -0.673690 -1.478427  0.524988\\n2000-01-07  0.577046  0.404705 -1.715002 -1.039268\\n2000-01-08 -1.157892 -0.370647 -1.344312  0.844885\\nYou may find this useful for applying a transform (in-place) to a subset of the\\ncolumns.\\nWarning\\npandas aligns all AXES when setting Series and DataFrame from .loc.\\nThis will not modify df because the column alignment is before value assignment.\"), Document(metadata={'language': 'en', 'source': 'https://pandas.pydata.org/docs/user_guide/indexing.html', 'title': 'Indexing and selecting data — pandas 2.2.2 documentation'}, page_content=\"In [21]: sa.a = 5\\nIn [22]: sa\\nOut[22]: \\na    5\\nb    2\\nc    3\\ndtype: int64\\nIn [23]: dfa.A = list(range(len(dfa.index)))  # ok if A already exists\\nIn [24]: dfa\\nOut[24]: \\n            A         B         C         D\\n2000-01-01  0  0.469112 -1.509059 -1.135632\\n2000-01-02  1  1.212112  0.119209 -1.044236\\n2000-01-03  2 -0.861849 -0.494929  1.071804\\n2000-01-04  3  0.721555 -1.039575  0.271860\\n2000-01-05  4 -0.424972  0.276232 -1.087401\\n2000-01-06  5 -0.673690 -1.478427  0.524988\\n2000-01-07  6  0.404705 -1.715002 -1.039268\\n2000-01-08  7 -0.370647 -1.344312  0.844885\\nIn [25]: dfa['A'] = list(range(len(dfa.index)))  # use this form to create a new column\"), Document(metadata={'language': 'en', 'source': 'https://pandas.pydata.org/docs/user_guide/merging.html', 'title': 'Merge, join, concatenate and compare — pandas 2.2.2 documentation'}, page_content='In [144]: df = pd.DataFrame(\\n   .....:     {\\n   .....:         \"col1\": [\"a\", \"a\", \"b\", \"b\", \"a\"],\\n   .....:         \"col2\": [1.0, 2.0, 3.0, np.nan, 5.0],\\n   .....:         \"col3\": [1.0, 2.0, 3.0, 4.0, 5.0],\\n   .....:     },\\n   .....:     columns=[\"col1\", \"col2\", \"col3\"],\\n   .....: )\\n   .....: \\nIn [145]: df\\nOut[145]: \\n  col1  col2  col3\\n0    a   1.0   1.0\\n1    a   2.0   2.0\\n2    b   3.0   3.0\\n3    b   NaN   4.0\\n4    a   5.0   5.0\\nIn [146]: df2 = df.copy()\\nIn [147]: df2.loc[0, \"col1\"] = \"c\"\\nIn [148]: df2.loc[2, \"col3\"] = 4.0\\nIn [149]: df2\\nOut[149]: \\n  col1  col2  col3\\n0    c   1.0   1.0\\n1    a   2.0   2.0\\n2    b   3.0   4.0\\n3    b   NaN   4.0\\n4    a   5.0   5.0'), Document(metadata={'language': 'en', 'source': 'https://pandas.pydata.org/docs/user_guide/groupby.html', 'title': 'Group by: split-apply-combine — pandas 2.2.2 documentation'}, page_content='In [225]: df\\nOut[225]: \\n  Branch Buyer  Quantity                Date\\n0      A  Carl         1 2013-01-01 13:00:00\\n1      A  Mark         3 2013-01-01 13:05:00\\n2      A  Carl         5 2013-10-01 20:00:00\\n3      A  Carl         1 2013-10-02 10:00:00\\n4      A   Joe         8 2013-10-01 20:00:00\\n5      A   Joe         1 2013-10-02 10:00:00\\n6      A   Joe         9 2013-12-02 12:00:00\\n7      B  Carl         3 2013-12-02 14:00:00\\nGroupby a specific column with the desired frequency. This is like resampling.'), Document(metadata={'source': 'websearch'}, page_content=\"And to add a prefix to the columns use: dummies.columns = ['D_'+col_name for col_name in dummies.columns] - Ufos. Commented Nov 12, 2017 at 23:06. 2 ... You can use str.join to join all elements in list present in series into string and then use str.get_dummies: ... Pandas convert dummies to a new column. Hot Network Questions\\nColumns in the output are each named after a value; if the input is\\na DataFrame, the name of the original variable is prepended to the value.\\n Examples\\nprevious\\npandas.concat\\nnext\\npandas.from_dummies\\n© 2024, pandas via NumFOCUS, Inc. Site Navigation\\nSite Navigation\\npandas.get_dummies#\\nConvert categorical variable into dummy/indicator variables.\\n If data contains other columns than the\\ndummy-coded one(s), these will be prepended, unaltered, to the result.\\n Whether the dummy-encoded columns should be backed by\\na SparseArray (True) or a regular NumPy array (False).\\n\\nThis is a non-exhaustive solution to specifying many different columns to get_dummies while excluding some columns. Using the built-in filter() function on df.columns is also an option. pd.get_dummies only works on columns with an object dtype when columns=None . Another potential option is to set only columns to be transformed with the object ...\")], 'web_fallback': False}}\n",
      "\n",
      "---\n",
      "\n",
      "---GENERATE---\n",
      "{'generate': {'candidate_answer': \"You can convert a column into dummies using the `pd.get_dummies` function in pandas. For example, `pd.get_dummies(df['column_name'])` will create dummy variables for the specified column. If you want to include the dummies in the original DataFrame, you can use `df = pd.get_dummies(df, columns=['column_name'])`.\", 'retries': 2}}\n",
      "\n",
      "---\n",
      "\n",
      "---FINALIZING THE RESPONSE---\n",
      "{'finalize_response': {'messages': [AIMessage(content=\"You can convert a column into dummies using the `pd.get_dummies` function in pandas. For example, `pd.get_dummies(df['column_name'])` will create dummy variables for the specified column. If you want to include the dummies in the original DataFrame, you can use `df = pd.get_dummies(df, columns=['column_name'])`.\")]}}\n",
      "\n",
      "---\n",
      "\n"
     ]
    }
   ],
   "source": [
    "VERBOSE = True\n",
    "inputs = {\"messages\": [(\"human\", \"how do i convert a column into dummies\")]}\n",
    "for output in graph.stream(inputs, {\"configurable\": {\"max_retries\": 1}}):\n",
    "    print(output)\n",
    "    print(\"\\n---\\n\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 41,
   "id": "5a8c74d2-3f34-4a48-8768-20ae28da525e",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2024-08-24T09:48:20.525926Z",
     "iopub.status.busy": "2024-08-24T09:48:20.525239Z",
     "iopub.status.idle": "2024-08-24T09:48:34.523668Z",
     "shell.execute_reply": "2024-08-24T09:48:34.521981Z",
     "shell.execute_reply.started": "2024-08-24T09:48:20.525871Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "---RETRIEVE---\n",
      "---GENERATE---\n",
      "---CHECK HALLUCINATIONS---\n",
      "---DECISION: GENERATION IS NOT GROUNDED IN DOCUMENTS, RE-TRY---\n",
      "---GENERATE---\n",
      "---CHECK HALLUCINATIONS---\n",
      "---DECISION: GENERATION IS NOT GROUNDED IN DOCUMENTS, RE-TRY---\n",
      "---GENERATE---\n",
      "---CHECK HALLUCINATIONS---\n",
      "---DECISION: GENERATION IS NOT GROUNDED IN DOCUMENTS, RE-TRY---\n",
      "---GENERATE---\n",
      "---CHECK HALLUCINATIONS---\n",
      "---DECISION: GENERATION IS NOT GROUNDED IN DOCUMENTS, RE-TRY---\n",
      "---RUNNING WEB SEARCH---\n",
      "---GENERATE---\n",
      "---FINALIZING THE RESPONSE---\n"
     ]
    },
    {
     "data": {
      "text/plain": [
       "{'messages': [HumanMessage(content='how do i convert a column into dummies', id='69125d7d-3b0e-42d7-8192-d43fd71ef25f'),\n",
       "  AIMessage(content=\"You can convert a column into dummies using the `pd.get_dummies` function in pandas. For example, `pd.get_dummies(df['column_name'])` will create dummy variables for the specified column. If you want to include the original DataFrame, you can use `pd.get_dummies(df, columns=['column_name'])`.\", id='c199ec88-cc3c-494d-adeb-3c7290bfe4b6')],\n",
       " 'question': 'how do i convert a column into dummies',\n",
       " 'documents': [Document(metadata={'language': 'en', 'source': 'https://pandas.pydata.org/docs/user_guide/indexing.html', 'title': 'Indexing and selecting data — pandas 2.2.2 documentation'}, page_content=\"In [7]: df[['B', 'A']] = df[['A', 'B']]\\nIn [8]: df\\nOut[8]: \\n                   A         B         C         D\\n2000-01-01 -0.282863  0.469112 -1.509059 -1.135632\\n2000-01-02 -0.173215  1.212112  0.119209 -1.044236\\n2000-01-03 -2.104569 -0.861849 -0.494929  1.071804\\n2000-01-04 -0.706771  0.721555 -1.039575  0.271860\\n2000-01-05  0.567020 -0.424972  0.276232 -1.087401\\n2000-01-06  0.113648 -0.673690 -1.478427  0.524988\\n2000-01-07  0.577046  0.404705 -1.715002 -1.039268\\n2000-01-08 -1.157892 -0.370647 -1.344312  0.844885\\nYou may find this useful for applying a transform (in-place) to a subset of the\\ncolumns.\\nWarning\\npandas aligns all AXES when setting Series and DataFrame from .loc.\\nThis will not modify df because the column alignment is before value assignment.\"),\n",
       "  Document(metadata={'language': 'en', 'source': 'https://pandas.pydata.org/docs/user_guide/indexing.html', 'title': 'Indexing and selecting data — pandas 2.2.2 documentation'}, page_content=\"In [21]: sa.a = 5\\nIn [22]: sa\\nOut[22]: \\na    5\\nb    2\\nc    3\\ndtype: int64\\nIn [23]: dfa.A = list(range(len(dfa.index)))  # ok if A already exists\\nIn [24]: dfa\\nOut[24]: \\n            A         B         C         D\\n2000-01-01  0  0.469112 -1.509059 -1.135632\\n2000-01-02  1  1.212112  0.119209 -1.044236\\n2000-01-03  2 -0.861849 -0.494929  1.071804\\n2000-01-04  3  0.721555 -1.039575  0.271860\\n2000-01-05  4 -0.424972  0.276232 -1.087401\\n2000-01-06  5 -0.673690 -1.478427  0.524988\\n2000-01-07  6  0.404705 -1.715002 -1.039268\\n2000-01-08  7 -0.370647 -1.344312  0.844885\\nIn [25]: dfa['A'] = list(range(len(dfa.index)))  # use this form to create a new column\"),\n",
       "  Document(metadata={'language': 'en', 'source': 'https://pandas.pydata.org/docs/user_guide/merging.html', 'title': 'Merge, join, concatenate and compare — pandas 2.2.2 documentation'}, page_content='In [144]: df = pd.DataFrame(\\n   .....:     {\\n   .....:         \"col1\": [\"a\", \"a\", \"b\", \"b\", \"a\"],\\n   .....:         \"col2\": [1.0, 2.0, 3.0, np.nan, 5.0],\\n   .....:         \"col3\": [1.0, 2.0, 3.0, 4.0, 5.0],\\n   .....:     },\\n   .....:     columns=[\"col1\", \"col2\", \"col3\"],\\n   .....: )\\n   .....: \\nIn [145]: df\\nOut[145]: \\n  col1  col2  col3\\n0    a   1.0   1.0\\n1    a   2.0   2.0\\n2    b   3.0   3.0\\n3    b   NaN   4.0\\n4    a   5.0   5.0\\nIn [146]: df2 = df.copy()\\nIn [147]: df2.loc[0, \"col1\"] = \"c\"\\nIn [148]: df2.loc[2, \"col3\"] = 4.0\\nIn [149]: df2\\nOut[149]: \\n  col1  col2  col3\\n0    c   1.0   1.0\\n1    a   2.0   2.0\\n2    b   3.0   4.0\\n3    b   NaN   4.0\\n4    a   5.0   5.0'),\n",
       "  Document(metadata={'language': 'en', 'source': 'https://pandas.pydata.org/docs/user_guide/groupby.html', 'title': 'Group by: split-apply-combine — pandas 2.2.2 documentation'}, page_content='In [225]: df\\nOut[225]: \\n  Branch Buyer  Quantity                Date\\n0      A  Carl         1 2013-01-01 13:00:00\\n1      A  Mark         3 2013-01-01 13:05:00\\n2      A  Carl         5 2013-10-01 20:00:00\\n3      A  Carl         1 2013-10-02 10:00:00\\n4      A   Joe         8 2013-10-01 20:00:00\\n5      A   Joe         1 2013-10-02 10:00:00\\n6      A   Joe         9 2013-12-02 12:00:00\\n7      B  Carl         3 2013-12-02 14:00:00\\nGroupby a specific column with the desired frequency. This is like resampling.'),\n",
       "  Document(metadata={'source': 'websearch'}, page_content=\"And to add a prefix to the columns use: dummies.columns = ['D_'+col_name for col_name in dummies.columns] - Ufos. Commented Nov 12, 2017 at 23:06. 2 ... You can use str.join to join all elements in list present in series into string and then use str.get_dummies: ... Pandas convert dummies to a new column. Hot Network Questions\\nColumns in the output are each named after a value; if the input is\\na DataFrame, the name of the original variable is prepended to the value.\\n Examples\\nprevious\\npandas.concat\\nnext\\npandas.from_dummies\\n© 2024, pandas via NumFOCUS, Inc. Site Navigation\\nSite Navigation\\npandas.get_dummies#\\nConvert categorical variable into dummy/indicator variables.\\n If data contains other columns than the\\ndummy-coded one(s), these will be prepended, unaltered, to the result.\\n Whether the dummy-encoded columns should be backed by\\na SparseArray (True) or a regular NumPy array (False).\\n\\nThis is a non-exhaustive solution to specifying many different columns to get_dummies while excluding some columns. Using the built-in filter() function on df.columns is also an option. pd.get_dummies only works on columns with an object dtype when columns=None . Another potential option is to set only columns to be transformed with the object ...\")],\n",
       " 'candidate_answer': \"You can convert a column into dummies using the `pd.get_dummies` function in pandas. For example, `pd.get_dummies(df['column_name'])` will create dummy variables for the specified column. If you want to include the original DataFrame, you can use `pd.get_dummies(df, columns=['column_name'])`.\",\n",
       " 'retries': 4,\n",
       " 'web_fallback': False}"
      ]
     },
     "execution_count": 41,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "graph.invoke(inputs)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "9ae91dc7-7555-4cea-bc10-634136561f12",
   "metadata": {},
   "outputs": [],
   "source": []
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "agent",
   "language": "python",
   "name": "agent"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.12.2"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
